import random
import numpy as np
class network:
    '''这是一个神经网络类

    Public attributes:
    - __active_functioins_and_derivative: 这是一个list，内部元素是元组tuple，tuple有两个元素，第一个是激活函数，第二个是激活函数的导数，建议不要在实例外部访问。
    - __networks: 这是一个list，内部元素是numpy里的ndarray类型的一个一个的矩阵。可以理解为第一个__networks的第一个元素就是第一层网络的权重矩阵，以此类推……
    - __lossfunction: 这是用户自定义后传进来的损失函数名。
    - __inputdatas: 这是训练的输入数据。shape为（n，m）。n是样本个数，m是每个样本的维度。
    - __labels: 这是训练数据的标签。注意：标签个数一定要一定要和inputdatas的样本个数大小一样。

    Availabel functions:
    - shuffleinputdata:这个方法的目的是打乱我们实例化神经网络时输入的traindata里的顺序。
    - optim：这个方法的就是随机梯度下降法的实现。
    '''




    def __init__(self,traindata,lossfunction,lossfunction_derivative,*layers):
        '''实例化一个神经网络

        Args:
            traindata: 这是一个二维列表，第一个元素是训练数据的样本（是numpy的ndarray类型，shape为（n，m），n是样本个数，m是每个样本的维度）
                第二个元素是训练数据的标签（是numpy的ndarray类型，shape为（n，m），n是样本标签个数，m是每个样本标签的维度）
            lossfunction_derivative: 这是一个函数类型的变量，用户自定义的损失函数的导数。
            lossfunction: 这是一个函数类型的变量，用户自定义的损失函数。
            layers: 这是可变参数，代表神经网络的架构，每层神经网络用元组来描述，例如(1, 4, logi_func, logi_func_derivative)代表这一层是1*4的神
            经网络，同时激活函数是自己定义的logi_func函数,logi_func_derivative是logi_func对应的导数。

        Returns:
            None
        '''
        self.layerlen = len(layers)
        self.__active_functioins_and_derivative = [(layer[2],layer[3]) for layer in layers]
        self.__networks = [np.random.rand(layer[0], layer[1]) for layer in layers]
        self.__lossfunction = lossfunction
        self.__lossfunction_derivative = lossfunction_derivative
        self.__inputdatas = traindata[0]
        self.__labels = traindata[1]
        self.tempinputdata = []
        self.inputdatamean = np.mean(self.__inputdatas,axis=0)
        self.inputdatastd = np.std(self.__inputdatas,axis=0)
        # for i,v in enumerate(traindata[0]):
        #     self.tempinputdata.append([v,self.__labels[i]])
        for (inputdata,lable) in zip(self.__inputdatas,self.__labels):
            self.tempinputdata.append([inputdata,lable])
        self.tempinputdata = np.array(self.tempinputdata)


    def shuffleinputdata(self):
        np.random.shuffle(self.tempinputdata)
        tempdatas=[]
        templabels=[]
        for v in self.tempinputdata:
            tempdatas.append(v[0])
            templabels.append(v[1])
        self.__inputdatas = np.array(tempdatas).reshape(len(tempdatas),1)
        self.__labels = np.array(templabels).reshape(len(templabels),1)
        random.shuffle(self.tempinputdata)


    def forward(self,inputdata):
        self.results = [inputdata]
        temp = inputdata
        self.gradfunc = []
        for i,network in enumerate(self.__networks):
            t = np.dot(temp, network)
            gradtemp = self.__active_functioins_and_derivative[i][1](t)
            temp = self.__active_functioins_and_derivative[i][0](t)
            self.results.append(temp)
            self.gradfunc.append(gradtemp)

    def backward(self,label):
        self.lossvaules = []
        temp = self.__lossfunction_derivative(self.results[-1],self.nomalize(label))
        self.lossvaules.append(temp)
        for network in reversed(self.__networks[1:len(self.__networks)]):
            temp = np.dot(network,temp)
            self.lossvaules.append(temp)

    def getloss(self,label):
        # print(self.results[-1])
        # print( self.nomalize(label))
        return self.__lossfunction(self.results[-1],self.nomalize(label)).mean()

    def nomalize(self,inputdata):
        return (inputdata-self.inputdatamean)/self.inputdatastd

    def optim(self,learn_rate):
        '''随机梯度下降的反向传播法

        这个方法将通过随机梯度下降的方向传播法将全部输入数据拿来训练

        Args:
            learn_rate: 学习速率，一般设置为0.001或者0.01之类量级的小数
        Returns:
            totalloss: 一次训练完所有数据之后的loss损失值总和。
        '''
        totalloss=0
        for (inputdata,label) in zip(self.__inputdatas,self.__labels):
            self.forward(self.nomalize(inputdata))
            self.backward(self.nomalize(label))
            # print(self.__networks)
            for i,(network,result,gradfunc) in enumerate(zip(self.__networks,self.results,self.gradfunc)):
                temp = []
                for j,layerloss in enumerate(self.lossvaules[self.layerlen-i-1]):
                    temp.append(layerloss*gradfunc[j])
                lossandgrad = np.array(temp).reshape(1,len(temp))
                self.__networks[i] =network+(np.dot(result.reshape(len(result),1),lossandgrad)*learn_rate)
            totalloss+=self.getloss(label)
        return totalloss

